iT邦幫忙

0

[Day17]檔案歸檔器:依「副檔名」或「日期」自動分類

  • 分享至 

  • xImage
  •  

把一堆散落的檔案,一鍵整理到結構化的資料夾:
模式一:依 副檔名 分類(例:jpg/ png/ pdf/ ...)
模式二:依 日期 分類(YYYY/MM/,用最後修改時間)
支援 move/copy、遞迴、萬用字元過濾
預設 Dry-run(只做計畫、不動檔)+輸出 CSV 日誌
若目標檔名已存在 → 自動加序號避免覆蓋

程式碼(存成 file_sorter.py)

# file_sorter.py — Day 17:依副檔名/日期歸檔(move/copy/試跑/CSV 紀錄)
from __future__ import annotations
import argparse, csv, shutil, time
from pathlib import Path
from typing import Iterable, List, Tuple

def iter_files(root: Path, recursive: bool, patterns: List[str] | None) -> Iterable[Path]:
    pats = patterns or ["*"]
    seen = set()
    for pat in pats:
        glob = root.rglob if recursive else root.glob
        for p in glob(pat):
            if p.is_file():
                rp = p.resolve()
                if rp not in seen:
                    seen.add(rp)
                    yield p

def date_folder(p: Path) -> Path:
    st = p.stat()
    y = time.strftime("%Y", time.localtime(st.st_mtime))
    m = time.strftime("%m", time.localtime(st.st_mtime))
    return Path(y) / m  # e.g. 2025/09

def ext_folder(p: Path) -> Path:
    ext = p.suffix.lower().lstrip(".") or "_noext"
    return Path(ext)

def unique_path(dst: Path) -> Tuple[Path, str]:
    """避免覆蓋:若存在就自動加 _1, _2...,回傳(路徑, 說明文字)"""
    if not dst.exists():
        return dst, ""
    base, ext = dst.stem, dst.suffix
    k = 1
    while True:
        alt = dst.with_name(f"{base}_{k}{ext}")
        if not alt.exists():
            return alt, f"rename({_kstr(k)})"
        k += 1

def _kstr(k: int) -> str:
    return str(k)

def write_csv(rows: List[dict], out: Path):
    out.parent.mkdir(parents=True, exist_ok=True)
    cols = ["action","src","dst","note"]
    with out.open("w", encoding="utf-8", newline="") as f:
        w = csv.DictWriter(f, fieldnames=cols)
        w.writeheader()
        for r in rows: w.writerow(r)

def main():
    ap = argparse.ArgumentParser(description="檔案歸檔器:依副檔名或日期分類(支援 move/copy/試跑/CSV)")
    ap.add_argument("--src", type=Path, required=True, help="來源資料夾")
    ap.add_argument("--dst", type=Path, required=True, help="目標資料夾")
    ap.add_argument("--by", choices=["ext","date"], default="ext", help="分類方式:ext=副檔名;date=YYYY/MM")
    ap.add_argument("--mode", choices=["move","copy"], default="move", help="動作:move 或 copy(預設 move)")
    ap.add_argument("--recursive", action="store_true", help="包含子資料夾")
    ap.add_argument("--match", nargs="*", help="萬用字元過濾,如 '*.jpg' '*.pdf'")
    ap.add_argument("--dry-run", action="store_true", help="試跑:只列出計畫,不真的移動/複製(預設開啟)")
    ap.add_argument("--apply", action="store_true", help="真的執行(與 --dry-run 互斥,二擇一)")
    ap.add_argument("--out", type=Path, default=Path("exports/sort_log.csv"), help="輸出 CSV 日誌路徑")
    args = ap.parse_args()

    if args.dry_run and args.apply:
        print("❌ --dry-run 與 --apply 不能同時使用"); return
    if not args.apply:
        args.dry_run = True  # 預設試跑

    src, dst = args.src, args.dst
    if not src.exists():
        print("❌ 來源不存在"); return
    dst.mkdir(parents=True, exist_ok=True)

    files = list(iter_files(src, args.recursive, args.match))
    if not files:
        print("⚠️ 找不到檔案(檢查 --match / 路徑)"); return

    rows: List[dict] = []
    count = 0

    for p in files:
        sub = ext_folder(p) if args.by == "ext" else date_folder(p)
        out_dir = dst / sub
        out_dir.mkdir(parents=True, exist_ok=True)

        target = out_dir / p.name
        final, note = unique_path(target)

        if args.dry_run:
            rows.append({"action": f"plan-{args.mode}", "src": str(p), "dst": str(final), "note": note})
        else:
            try:
                if args.mode == "move":
                    shutil.move(str(p), str(final))
                else:
                    shutil.copy2(str(p), str(final))
                rows.append({"action": args.mode, "src": str(p), "dst": str(final), "note": note})
                count += 1
            except Exception as e:
                rows.append({"action": "error", "src": str(p), "dst": str(final), "note": str(e)})

    write_csv(rows, args.out)
    if args.dry_run:
        print(f"📝 試跑完成,{len(rows)} 筆計畫已輸出:{args.out}")
    else:
        print(f"✅ 已{args.mode} {count} 個檔案;明細:{args.out}")

if __name__ == "__main__":
    main()

怎麼用(PowerShell)

# 1) 先「試跑」:目前資料夾的 PDF/JPG,依副檔名分類到 .\sorted\
python .\file_sorter.py --src . --dst .\sorted --by ext --recursive --match '*.pdf' '*.jpg'
ii .\exports   # 看 sort_log.csv 計畫

# 2) 真的執行(移動)
python .\file_sorter.py --src . --dst .\sorted --by ext --recursive --match '*.pdf' '*.jpg' --apply

# 3) 依「日期」分類(YYYY\MM),使用複製模式
python .\file_sorter.py --src . --dst .\backup --by date --recursive --mode copy --apply

# 4) 只整理目前資料夾(不含子資料夾)、所有檔案
python .\file_sorter.py --src . --dst .\sorted --by ext --apply

實作:
https://ithelp.ithome.com.tw/upload/images/20250930/20169368WUn90j67US.png
重點 & 小提醒
預設 Dry-run:不動檔案,只輸出 CSV 日誌(exports\sort_log.csv),安全先看計畫再決定。
衝突自動改名:目標檔已存在時自動加 _1、_2…,note 會標 rename(k)。
PowerShell 萬用字元要加引號:'.pdf'、'.jpg'。
--by date 使用檔案最後修改時間(mtime)決定 YYYY/MM 目錄。

今日小結
完成一支「檔案歸檔器」:副檔名/日期兩種分類法、支援 move/copy、試跑與 CSV 紀錄


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言